<?php


namespace app\common;

class Snowflake
{

    const EPOCH = 1609459200000; // 自定义起始时间戳（2021-01-01 00:00:00）
    const DATA_CENTER_ID_BITS = 5;
    const WORKER_ID_BITS = 5;
    const SEQUENCE_BITS = 12;

    const MAX_DATA_CENTER_ID = -1 ^ (-1 << self::DATA_CENTER_ID_BITS);
    const MAX_WORKER_ID = -1 ^ (-1 << self::WORKER_ID_BITS);
    const MAX_SEQUENCE = -1 ^ (-1 << self::SEQUENCE_BITS);

    const WORKER_ID_SHIFT = self::SEQUENCE_BITS;
    const DATA_CENTER_ID_SHIFT = self::SEQUENCE_BITS + self::WORKER_ID_BITS;
    const TIMESTAMP_LEFT_SHIFT = self::SEQUENCE_BITS + self::WORKER_ID_BITS + self::DATA_CENTER_ID_BITS;

    private $dataCenterId;
    private $workerId;
    private $sequence = 0;
    private $lastTimestamp = -1;

    public function __construct($dataCenterId, $workerId)
    {
        if ($dataCenterId > self::MAX_DATA_CENTER_ID || $dataCenterId < 0) {
            throw new \InvalidArgumentException("Data center ID must be between 0 and " . self::MAX_DATA_CENTER_ID);
        }
        if ($workerId > self::MAX_WORKER_ID || $workerId < 0) {
            throw new \InvalidArgumentException("Worker ID must be between 0 and " . self::MAX_WORKER_ID);
        }
        $this->dataCenterId = $dataCenterId;
        $this->workerId = $workerId;
    }

    public function nextId()
    {
        $timestamp = $this->getCurrentTimestamp();

        if ($timestamp < $this->lastTimestamp) {
            throw new \Exception("Clock moved backwards. Refusing to generate id for " . ($this->lastTimestamp - $timestamp) . " milliseconds");
        }

        if ($timestamp == $this->lastTimestamp) {
            $this->sequence = ($this->sequence + 1) & self::MAX_SEQUENCE;
            if ($this->sequence == 0) {
                $timestamp = $this->waitNextMillis($this->lastTimestamp);
            }
        } else {
            $this->sequence = 0;
        }

        $this->lastTimestamp = $timestamp;

        return (($timestamp - self::EPOCH) << self::TIMESTAMP_LEFT_SHIFT) |
            ($this->dataCenterId << self::DATA_CENTER_ID_SHIFT) |
            ($this->workerId << self::WORKER_ID_SHIFT) |
            $this->sequence;
    }

    private function getCurrentTimestamp()
    {
        return floor(microtime(true) * 1000);
    }

    private function waitNextMillis($lastTimestamp)
    {
        $timestamp = $this->getCurrentTimestamp();
        while ($timestamp <= $lastTimestamp) {
            $timestamp = $this->getCurrentTimestamp();
        }
        return $timestamp;
    }
}